#ifndef BLOCKWISE_INTEGRATED_BLOCK_HPP
#define BLOCKWISE_INTEGRATED_BLOCK_HPP

#include "bi_location.hpp"
#include "bi_indexHelper.hpp"
#include "bi_reconstruction.hpp"
#include "bi_edgeRow.hpp"
#include "typeAliases.hpp"
#include "typeHelpers.hpp"
#include <experimental/memory>

namespace hms::swe
{

using std::experimental::observer_ptr;
using std::experimental::make_observer;


template<BlockLocation loc>
class Block {
public:
	
	static constexpr BlockLocation location {loc};
	static constexpr Index cb { addBottom<loc>() };
	static constexpr Index ct { addTop   <loc>() };
	static constexpr Index cl { addLeft  <loc>() };
	static constexpr Index cr { addRight <loc>() };
	static constexpr Index nol { IndexHelper::_Mesh::nOuterLayers };
private:
	Array2i m_firstCell;
	Array2i m_size;
	Array2i m_sizeRead;
	Index m_nWrite;
	Index m_nRead;
	observer_ptr<const IndexHelper> m_indexHelper {nullptr};
	Array2i m_firstEdges;
	Array2i m_firstLR_h_block;
	Array2i m_firstLR_v_block;
	Array2i m_firstLR_h_mesh;
	Array2i m_firstLR_v_mesh;
public:
	Block() = default;
	Block(
		const IndexHelper& ihArg,
		Index startX, Index startY,
		Index sizeX, Index sizeY
	) :
		m_firstCell {startX, startY},
		m_size { sizeX,  sizeY},
		m_sizeRead {sizeX + cl + cr, sizeY + cb + ct},
		m_nWrite { m_size.prod() },
		m_nRead  { m_sizeRead.prod() }
	{
		indexHelper(ihArg);
		/* first horizontal edge is xBeg-ng + (yBeg-ng)*mesh.nx,
		 * first  vertical  edge is xBeg-ng + (yBeg-ng)*(mesh.nx+1) + nh,
		 * with nh being the number of horizontal edges */
		m_firstEdges = (m_firstCell - ih().mesh.ng).transpose().matrix() *
			// Reshaped<Vector4i,2,2>( { 1, ih().mesh.nx, 1, ih().mesh.nx+1 } );
			Vector4i{ 1, ih().mesh.nx, 1, ih().mesh.nx+1 }.reshaped(2,2);
		/* add nh */
		m_firstEdges[1] += ih().mesh.nx * (ih().mesh.ny + 1);

		/* left and right cell indices (block) of first horizontal edge */
		m_firstLR_h_block = {
			cl + nxRead()*(cb-1),
			cl + nxRead()* cb
		};
		/* left and right cell indices (mesh) of first horizontal edge */
		m_firstLR_h_mesh = {
			xBeg() + ih().mesh.nxg*(yBeg()-1),
			xBeg() + ih().mesh.nxg* yBeg()
		};
		/* left and right cell indices (block) of first vertical edge */
		m_firstLR_v_block = {
			m_firstLR_h_block[1] - 1,
			m_firstLR_h_block[1]
		};
		/* left and right cell indices (mesh) of first vertical edge */
		m_firstLR_v_mesh = {
			m_firstLR_h_mesh[1] - 1,
			m_firstLR_h_mesh[1]
		};
	}

	void indexHelper( const IndexHelper& ih ){
		m_indexHelper = make_observer( &ih );
	}
	auto indexHelper() const -> const IndexHelper& {
		assert( m_indexHelper );
		return *m_indexHelper;
	}
	auto ih() const -> const IndexHelper& { return indexHelper(); }
	auto firstCell() const -> const Array2i& { return m_firstCell; }
	auto size() const -> const Array2i& { return m_size; }
	auto sizeRead() const -> const Array2i& { return m_sizeRead; }
	auto nRead () const -> Index { return m_nRead; }
	auto nWrite() const -> Index { return m_nWrite; }
	auto xBeg() const -> const Index& { return m_firstCell.x(); }
	auto yBeg() const -> const Index& { return m_firstCell.y(); }
	auto nx() const -> const Index& { return m_size.x(); }
	auto ny() const -> const Index& { return m_size.y(); }
	auto nxRead() const -> const Index& { return m_sizeRead.x(); }
	auto nyRead() const -> const Index& { return m_sizeRead.y(); }

	auto hBeg() const -> const Index& { return m_firstEdges[0]; }
	auto vBeg() const -> const Index& { return m_firstEdges[1]; }

	template<EdgeRange orientation>
	auto lrBeg_block() const -> const Array2i& {
		if constexpr ( orientation == EdgeRange::horizontal )
			return m_firstLR_h_block;
		else
			return m_firstLR_v_block;
	}
	template<EdgeRange orientation>
	auto lrBeg_mesh() const -> const Array2i& {
		if constexpr ( orientation == EdgeRange::horizontal )
			return m_firstLR_h_mesh;
		else
			return m_firstLR_v_mesh;
	}

	template<ReconstructionType recType, EdgeRange orien>
	Array2i lrBeg_mesh( Index y ) const {
		return
			  lrBeg_mesh<orien>()
			+ ih().mesh.nxg * y
			+ cellOffset<recType, orien>();
	}
	

	template<int nComponents>
	ArrayNXsMap<nComponents> map( ArrayNXs<nComponents>& mem ) const {
		return { mem.data(), nComponents, nRead() };
	}

	template<typename T>
	using MeshBlock = Eigen::Block<Eigen::Reshaped<std::remove_reference_t<T>>>;

	template<Index nComponents, typename T>
	auto readBlock( T&& arr ) const -> MeshBlock<T> {
		return arr
			.reshaped(
				ih().mesh.nxg * nComponents,
				ih().mesh.nyg
			).block(
				(xBeg() - cl) * nComponents,
				yBeg() - cb,
				nxRead() * nComponents,
				nyRead()
			);
	}

	template<Index nComponents, typename T>
	auto writeBlock( T&& arr ) const -> MeshBlock<T>
	{
		return arr
			.reshaped(
				ih().mesh.nxg*nComponents,
				ih().mesh.nyg
			).block(
				xBeg() * nComponents,
				yBeg(),
				nx() * nComponents,
				ny()
			);
	}

	template<int nComponents>
	auto writeBlock_mem( ArrayNXsMap<nComponents>& mem ) const
		-> MeshBlock<ArrayNXsMap<nComponents>>
	{
		return mem
			.reshaped( nxRead() * nComponents, nyRead() )
			.block( cl*nComponents, cb, nx()*nComponents, ny() );
	}

	/* doesn't work that well with nested types yet */
	template<typename T, Index nComponents>
	using maybe_const_Map =
		std::conditional_t<
			std::is_same_v<MeshBlock<const ArrayXXs>, remove_qualifiers<T>>,
			ArrayNXsCMap<nComponents>,
			std::conditional_t<
				std::is_same_v<MeshBlock<ArrayXXs>, remove_qualifiers<T>>,
				ArrayNXsMap<nComponents>,
				Eigen::Map<transcribe_const_t<T, ArrayNXs<nComponents>>>
			>
		>;

	template<Index nComponents, typename T>
	auto rowMap( T&& block, Index y ) const -> maybe_const_Map<T, nComponents>
	{
		return { block.col(y).data(), nComponents, nx() };
	}

	template<Index nComponents, typename T>
	auto rowMapRead( T&& block, Index y ) const -> maybe_const_Map<T, nComponents>
	{
		return { block.col(y).data(), nComponents, nxRead() };
	}

	template<int nComponents>
	auto rowMap_mem( ArrayNXsMap<nComponents>& mem, Index y ) const
		-> ArrayNXsMap<nComponents>
	{
		return { writeBlock_mem(mem).col(y).data(), nComponents, nx() };
	}

	/* the number of vertical edges for left and right blocks of a certain
	 * reconstruction type is a constexpr number (2 central, 1 muscl).
	 * For all other cases, it's nx (the block size in x) or nx+1. */
	template<ReconstructionType recType, EdgeRange orien>
	constexpr Index nEdgesForMap() const {
		using RT = ReconstructionType;
		using ER = EdgeRange;
		if constexpr ( atLeft<loc>() || atRight<loc>() ){
			if constexpr (
				orien == ER::vertical && recType == RT::secondOrder
			){
				return 1;
			} else {
				return nol;
			}
		} else {
			if constexpr ( orien == ER::horizontal ){
				return nx();
			} else {
				return nx() + 1;
			}
		}
	}

	/* offset in number of cells inside a row. For mapping to rows of a 
	 * non-inner block, whose vertical edges at the block boundary otherwise
	 * get the wrong cell data addresses */
	template<ReconstructionType recType, EdgeRange orien>
	static constexpr Index cellOffset(){
		using RT = ReconstructionType;
		using ER = EdgeRange;
		/* second order vertical edges are at the right of left blocks */
		if constexpr (
			   atLeft<loc>()
			&& orien == ER::vertical
			&& recType == RT::secondOrder
		){
			return nol;
		/* the first vertical edge of left blocks is second order,
		 * but afterwards they're central */
		} else if constexpr (
			   atRight<loc>()
			&& orien == ER::vertical
			&& recType != RT::secondOrder
		){
			return 1;
		} else {
			return 0;
		}
	}

	/* by taking both the reconstruction type and edge orientation,
	 * we can tell at compile time at which side of the block we are
	 * and therefore with which index to start */
	template<
		typename MapType,
		typename BaseType,
		Index coeffOffset,
		bool second,
		ReconstructionType recType,
		EdgeRange orien,
		typename T
	>
	auto mapLR( T&& readBlock, const EdgeRow<recType,orien>& er ) const
		-> Pair<MapType>
	{
		static constexpr bool horiz { orien == EdgeRange::horizontal };
		static constexpr Index
			cOff { cellOffset<recType, orien>() },
			nComp     {  MapType::RowsAtCompileTime },
			nCompBase { BaseType::RowsAtCompileTime };

		static constexpr Index
			lcol { cb - (horiz? 1 + second : 0) },
			rcol { cb + (horiz?     second : 0) },
			loff { nCompBase*(cl + (horiz? 0 : -1 - second + cOff)) },
			roff { nCompBase*(cl + (horiz? 0 :      second + cOff)) };
	
		static_assert( nComp != Dynamic );

		return {{ {
			readBlock.col(lcol+er.i).data() + loff + coeffOffset,
			nComp, er.nEdges
		}, {
			readBlock.col(rcol+er.i).data() + roff + coeffOffset,
			nComp, er.nEdges
		} }};
	}

	// template<
	// 	typename MapType,
	// 	typename BaseType,
	// 	Index coeffOffset,
	// 	bool second,
	// 	ReconstructionType recType,
	// 	EdgeRange orien,
	// 	typename T
	// >
	// auto mapLR( T&& readBlock, Index y ) const
	// 	-> Pair<MapType>
	// {
	// 	static constexpr bool horiz { orien == EdgeRange::horizontal };
	// 	static constexpr Index cOff { cellOffset<recType, orien>() };
	// 	static constexpr Index
	// 		lcol { cb - (horiz? 1 + second : 0) },
	// 		rcol { cb + (horiz?     second : 0) },
	// 		loff { nCompFrom*(cl + (horiz? 0 : -1 - second + cOff)) },
	// 		roff { nCompFrom*(cl + (horiz? 0 :      second + cOff)) };
	// 	Index nEdges { nEdgesForMap<recType, orien>() };
	
	// 	static constexpr Index nComp { MapType::RowsAtCompileTime };
	// 	static_assert( nComp != Dynamic );

	// 	return {{
	// 		{readBlock.col(lcol+y).data() + loff + coeffOffset, nComp, nEdges},
	// 		{readBlock.col(rcol+y).data() + roff + coeffOffset, nComp, nEdges},
	// 	}};
	// }

	template<
		typename MapType,
		bool second,
		ReconstructionType recType,
		EdgeRange orien,
		typename T
	>
	auto mapLR( T&& readBlock, const EdgeRow<recType,orien>& er ) const
		-> Pair<MapType>
	{
		return mapLR<
			MapType, MapType, 0, second
			// MapType, MapType, 0, recType, orien, second, T
		>( std::forward<T>(readBlock), er );
	}


	template<typename Callable, typename ... Ts>
	void forEachRow( Callable&& func, Ts&& ... args ) const {
		static_assert(
			std::is_invocable_v<Callable, Index, std::forward<Ts> ...>,
			"Callable must be invocable with row index as first argument!"
		);
		for ( Index y=0; y<ny(); ++y )
			func(y, std::forward<Ts>(args) ... );
	}
};

} // namespace hms::swe


#endif